<!DOCTYPE html>
<html>
<head>
    <title>Simple PLY Viewer</title>
    <style>
        body { 
            margin: 0; 
            font-family: Arial, sans-serif;
            background: #222;
        }
        canvas { 
            display: block; 
            width: 100%;
            height: 80vh;
        }
        #upload-container {
            padding: 20px;
            background: #333;
            border-bottom: 1px solid #444;
            color: white;
        }
        #file-input {
            display: none;
        }
        .upload-btn {
            background: #4CAF50;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        .upload-btn:hover {
            background: #45a049;
        }
        #loading {
            display: none;
            margin-top: 10px;
            color: #aaa;
        }
        #controls {
            padding: 10px;
            background: #333;
            color: white;
        }
        .control-btn {
            padding: 8px 12px;
            margin-right: 5px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            background: #555;
            color: white;
        }
        .control-btn:hover {
            background: #666;
        }
        #instructions {
            position: absolute;
            bottom: 10px;
            left: 10px;
            color: white;
            background: rgba(0,0,0,0.5);
            padding: 10px;
            border-radius: 5px;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div id="upload-container">
        <label for="file-input" class="upload-btn">Select PLY/DRC Files</label>
        <input id="file-input" type="file" accept=".ply,.drc" multiple>
        <div id="loading">Loading...</div>
    </div>
    <div id="controls">
        <button id="rotate-toggle" class="control-btn">Pause Rotation</button>
        <button id="reset-view" class="control-btn">Reset View</button>
    </div>
    <div id="instructions">
        Controls: WASD to move, Mouse drag to look around
    </div>

    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/PLYLoader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/DRACOLoader.js"></script>
    <script>
        // Initialize scene
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x222222);
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ 
            antialias: true,
            alpha: true // 允许透明背景
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        
        // Movement variables
        const moveSpeed = 0.01;
        const maxDistance = 0.3; // Maximum movement distance from origin
        const keys = {
            w: false,
            a: false,
            s: false,
            d: false
        };
        let isMouseDown = false;
        let previousMousePosition = {
            x: 0,
            y: 0
        };
        let rotationSpeed = 0.0005;
        let isRotating = true;
        let sceneCenter = new THREE.Vector3();
        let animationId = null;
        let isAtLimit = false;

        // Event listeners for keyboard controls
        document.addEventListener('keydown', (event) => {
            switch (event.key.toLowerCase()) {
                case 'w': keys.w = true; break;
                case 'a': keys.a = true; break;
                case 's': keys.s = true; break;
                case 'd': keys.d = true; break;
            }
            // Restart animation if it was stopped
            if (!animationId && (keys.w || keys.a || keys.s || keys.d)) {
                animate();
            }
        });

        document.addEventListener('keyup', (event) => {
            switch (event.key.toLowerCase()) {
                case 'w': keys.w = false; break;
                case 'a': keys.a = false; break;
                case 's': keys.s = false; break;
                case 'd': keys.d = false; break;
            }
        });

        // Mouse drag for camera-relative rotation
        renderer.domElement.addEventListener('mousedown', (event) => {
            isMouseDown = true;
            previousMousePosition = {
                x: event.clientX,
                y: event.clientY
            };
            // Prevent default to avoid text selection
            event.preventDefault();
        });

        document.addEventListener('mouseup', () => {
            isMouseDown = false;
        });

        document.addEventListener('mousemove', (event) => {
            if (isMouseDown) {
                const deltaMove = {
                    x: event.clientX - previousMousePosition.x,
                    y: event.clientY - previousMousePosition.y
                };
                
                const quaternion = new THREE.Quaternion();
                
                // Horizontal rotation (Y-axis)
                const yQuaternion = new THREE.Quaternion();
                yQuaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), -deltaMove.x * 0.002);
                quaternion.multiply(yQuaternion);
                
                // Vertical rotation (X-axis)
                const xQuaternion = new THREE.Quaternion();
                xQuaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0), -deltaMove.y * 0.002);
                quaternion.multiply(xQuaternion);
                
                // Apply rotation
                camera.quaternion.multiply(quaternion);
                
                previousMousePosition = {
                    x: event.clientX,
                    y: event.clientY
                };
            }
        });

        // Prevent context menu on canvas
        renderer.domElement.addEventListener('contextmenu', (event) => {
            event.preventDefault();
        });

        // initialize all loader
        const loader = new THREE.PLYLoader();
        const dracoLoader = new THREE.DRACOLoader();
        dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/libs/draco/');
        let loadedCount = 0;
        let totalFiles = 0;

        // File upload handler
        document.getElementById('file-input').addEventListener('change', function(e) {
            const files = e.target.files;
            if (files.length === 0) return;
            
            document.getElementById('loading').style.display = 'block';
            totalFiles = files.length;
            loadedCount = 0;
            
            // Clear existing models from scene
            scene.children.slice().forEach(child => {
            if (child instanceof THREE.Mesh) {
                if (child.geometry) child.geometry.dispose();
                if (child.material) child.material.dispose();
                scene.remove(child);
            }
            });
            
            // Load each PLY file
            Array.from(files).forEach((file, index) => {
                const reader = new FileReader();
                reader.onload = function(event) {
                    try {
                        if (file.name.endsWith('.ply')) {
                            const geometry = loader.parse(event.target.result);
                            const material = new THREE.MeshBasicMaterial({
                                side: THREE.DoubleSide,
                                vertexColors: true,
                            });
                            const mesh = new THREE.Mesh(geometry, material);
                            mesh.rotateX(-Math.PI / 2);
                            mesh.rotateZ(-Math.PI / 2);
                            scene.add(mesh);
                        } else if (file.name.endsWith('.drc')) {
                            // Draco file handling
                            dracoLoader.decodeDracoFile(event.target.result, function(geometry) {
                                // Compute normals for Draco geometry if missing
                                if (!geometry.attributes.normal) {
                                    geometry.computeVertexNormals();
                                }
                                const material = new THREE.MeshBasicMaterial({
                                    side: THREE.DoubleSide,
                                    vertexColors: true,
                                });
                                const mesh = new THREE.Mesh(geometry, material);
                                mesh.rotateX(-Math.PI / 2);
                                mesh.rotateZ(-Math.PI / 2);
                                scene.add(mesh);
                            });
                        }
                        
                        loadedCount++;
                        if(loadedCount === totalFiles) {
                            document.getElementById('loading').style.display = 'none';
                            positionCamera();
                            isRotating = true;
                            document.getElementById('rotate-toggle').textContent = 'Pause Rotation';
                            animate(); // Start animation after loading
                        }
                    } catch (error) {
                        console.error('Error loading PLY file:', error);
                        loadedCount++;
                        if(loadedCount === totalFiles) {
                            document.getElementById('loading').style.display = 'none';
                            if (scene.children.filter(c => c instanceof THREE.Mesh).length > 0) {
                                positionCamera();
                                isRotating = true;
                                document.getElementById('rotate-toggle').textContent = 'Pause Rotation';
                                animate(); // Start animation after loading
                            }
                        }
                    }
                };
                reader.readAsArrayBuffer(file);
            });
        });
        
        // Position camera reset
        function positionCamera() {
            // Initial camera position
            scene.rotation.y = 0;
            camera.position.set(0, 0, 0);
            camera.lookAt(0, 0, -10);
        }
        
        // Control button events
        document.getElementById('rotate-toggle').addEventListener('click', function() {
            isRotating = !isRotating;
            this.textContent = isRotating ? 'Pause Rotation' : 'Start Rotation';
        });
        
        document.getElementById('reset-view').addEventListener('click', function() {
            positionCamera();
            isAtLimit = false;
            isRotating = true;
            if (!animationId) {
                animate();
            }
        });
        
        // Function to limit movement distance
        function limitMovement(position) {
            const distance = Math.sqrt(position.x * position.x + position.z * position.z);
            if (distance > maxDistance) {
                const ratio = maxDistance / distance;
                position.x *= ratio;
                position.z *= ratio;
                return true; // Reached limit
            }
            return false; // Not at limit
        }
        
        // Animation loop
        function animate() {
            // Calculate movement direction based on camera orientation
            if (keys.w || keys.a || keys.s || keys.d) {
                // Get camera's forward vector (negative Z-axis)
                const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
                // Get camera's right vector (positive X-axis)
                const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
                
                // Ignore Y component to keep movement horizontal
                forward.y = 0;
                right.y = 0;
                forward.normalize();
                right.normalize();
                
                // Calculate movement direction
                const movement = new THREE.Vector3();
                
                if (keys.w) movement.add(forward); // Forward
                if (keys.s) movement.sub(forward); // Backward
                if (keys.a) movement.sub(right);    // Left
                if (keys.d) movement.add(right);   // Right
                
                // Only normalize if there's actual movement
                if (movement.length() > 0) {
                    movement.normalize().multiplyScalar(moveSpeed);
                }
                
                // Store current Y position
                const currentY = camera.position.y;
                
                // Apply movement
                camera.position.add(movement);
                
                // Restore Y position to keep movement horizontal
                camera.position.y = currentY;
                
                // Check if reached movement limit
                isAtLimit = limitMovement(camera.position);
            }
            
            // Auto-rotation if enabled
            if (isRotating && scene.children.some(c => c instanceof THREE.Mesh)) {
                scene.rotation.y += rotationSpeed;
            }
            
            // Create a target position slightly in front of the camera
            const targetPosition = new THREE.Vector3();
            targetPosition.copy(camera.position);
            targetPosition.add(new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion));
            
            // Smoothly look at a point slightly in front of the camera
            camera.lookAt(targetPosition);
            
            renderer.render(scene, camera);
            
            // keep running
            animationId = requestAnimationFrame(animate);
        }
        
        // Initial animation start
        animate();

        // Window resize handler
        window.addEventListener('resize', function() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    </script>
</body>
</html>